package com.gaojinqi.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;


/**
 * 分布式锁抽象类 <br/>
 * 可自动续租     <br/>
 * <p>
 *
 * @author gaojinqi
 * @version 1.0
 * @since 2020年07月07日
 */
@Slf4j
public abstract class AbstractDistributedLock implements IDistributedLock {

    /**
     * Redis锁前缀
     */
    protected static final String LOCK_PREFIX = "redis_lock";
    /**
     * 用于存放守护线程
     */
    private static ThreadLocal<RenewThread> RENEW_THREA_LOCAL = new ThreadLocal<>();
    /**
     * 用于存放token
     */
    private static ThreadLocal<String> TOKEN_THREA_LOCAL = new ThreadLocal<>();

    protected Executor executor;

    public AbstractDistributedLock(Executor executor) {
        this.executor = executor;
    }

    /**
     * SETNX（Set if not exists） ，必须是原子操作    <br/>
     * 如果分布式锁中不存在key，则对key设置value，并且设置有限时长
     *
     * @param realKey    key
     * @param token      value
     * @param expireTime 超时时间
     * @param timeUnit   时间单位
     * @return 是否设置value成功
     */
    protected abstract boolean setIfAbsent(String realKey, String token, long expireTime, TimeUnit timeUnit);

    /**
     * CAS（Compare And Set），必须是原子操作  <br/>
     * 比较分布式锁中的key的oldValue是否与传入的value相等，相等则修改为value；不相等则无效
     *
     * @param realKey key
     * @param token   value
     * @return 是否修改成功
     */
    protected abstract boolean compareAndSet(String realKey, String token, long expireTime, TimeUnit timeUnit);

    /**
     * CAD（Compare And Delete），必须是原子操作  <br/>
     * 比较分布式锁中的key的oldValue是否与传入的value相等，相等则删除该key；不相等则无效
     *
     * @param realKey key
     * @param token   value
     * @return 是否删除成功
     */
    protected abstract boolean compareAndDelete(String realKey, String token);

    /**
     * 比较分布式锁中的key的oldValue是否与传入的value相等，相等则返回true；不相等则返回false
     *
     * @param realKey key
     * @param token   value
     */
    protected abstract boolean compare(String realKey, String token);

    public boolean tryLock(String key, long expireTime, TimeUnit timeUnit) {
        String realKey = buildKey(key);
        String token = buildToken();
        boolean acquired = false;
        try {
            acquired = this.setIfAbsent(realKey, token, expireTime, timeUnit);
        } catch (Exception e) {
            log.error(String.format(
                    "acquire lock occured an exception, key = %s, token = %s, currentThread = %s",
                    realKey, token, Thread.currentThread().getName()), e);
        }
        if (acquired) {
            TOKEN_THREA_LOCAL.set(token);
            // 获取到锁，启动续租守护线程
            RenewThread daemonThread =
                    new RenewThread(Thread.currentThread(), realKey, token, expireTime, timeUnit);
            daemonThread.setDaemon(true);
            RENEW_THREA_LOCAL.set(daemonThread);
            if (executor != null) {
                executor.execute(daemonThread);
            } else {
                daemonThread.start();
            }
            /*if (log.isDebugEnabled()) {
                log.debug("启动守护线程成功，daemonThread = {}", daemonThread);
            }*/
            return true;
        }
        return false;
    }

    public boolean unlock(String key) {
        String realKey = buildKey(key);
        String token = TOKEN_THREA_LOCAL.get();
        try {
            if (compareAndDelete(realKey, token)) {
                // 释放锁成功
                return true;
            }
        } catch (Exception e) {
            log.error(String.format(
                    "release lock occured an exception, key = %s, token = %s, currentThread = %s",
                    realKey, token, Thread.currentThread().getName()), e);
        } finally {
            this.clean();
        }
        return false;
    }

    void clean() {
        // 移除线程中的token
        TOKEN_THREA_LOCAL.remove();
        // 停止续租线程
        RenewThread daemonThread = RENEW_THREA_LOCAL.get();
        if (daemonThread != null) {
            //daemonThread.interrupt(); // 不能及时中断
            daemonThread.stopRenew();
            RENEW_THREA_LOCAL.remove();
            /*if (log.isDebugEnabled()) {
                log.debug("停止守护线程成功，daemonThread = {}", daemonThread);
            }*/
        }
    }

    /**
     * 自定义线程池
     */
    public void setExecutor(Executor executor) {
        this.executor = executor;
    }

    /**
     * 构建主键
     */
    protected String buildKey(String key) {
        return String.format("%s:%s", LOCK_PREFIX, key);
    }

    /**
     * 构建凭证
     */
    protected String buildToken() {
        return System.currentTimeMillis() + UUID.randomUUID().toString();
    }

    /**
     * 续租线程
     */
    class RenewThread extends Thread {

        private String realKey;
        private String token;
        private Long expireTime;
        private TimeUnit timeUnit;
        private Thread mainThread;
        /**
         * 是否续租
         */
        private volatile AtomicBoolean renewing;

        public RenewThread(Thread mainThread, String realKey, String token, Long expireTime, TimeUnit timeUnit) {
            this.realKey = realKey;
            this.token = token;
            this.expireTime = expireTime;
            this.timeUnit = timeUnit;
            this.mainThread = mainThread;
            renewing = new AtomicBoolean(true);
        }

        /**
         * 停止续租
         */
        public void stopRenew() {
            renewing.compareAndSet(true, false);
        }

        /**
         * 是否能续租
         */
        public boolean canRenew() {
            // 是否续租
            boolean renewing = this.renewing.get();
            if (!renewing) {
                return false;
            }
            // 守护线程是否可用
            Thread daemonThread = Thread.currentThread();
            boolean daemonEnable = !daemonThread.isInterrupted() && daemonThread.isAlive();
            if (!daemonEnable) {
                return false;
            }
            // 主线程是否可用
            boolean mainThreadEnable = this.mainThread != null && this.mainThread.isAlive() && !this.mainThread.isInterrupted();
            if (!mainThreadEnable) {
                return false;
            }
            // token校验是否准确
            boolean tokenEnable = compare(this.realKey, this.token);
            if (!tokenEnable) {
                return false;
            }
            return true;
        }


        @Override
        public void run() {
            // 计算续租时间，每1/3超时时间续租一次
            long renewExpireTime = expireTime / 3;
            while (this.renewing.get()) {
                try {
                    timeUnit.sleep(renewExpireTime);
                } catch (InterruptedException e) {
                    log.error(e.getMessage(), e);
                }
                try {
                    if (!canRenew()) {
                        break;
                    }
                    // 续租
                    boolean success = compareAndSet(realKey, token, expireTime, timeUnit);
                    if (success && log.isDebugEnabled()) {
                        log.debug("renew success, renew lock every {} ms, info = {}", TimeUnit.MILLISECONDS.toMillis(renewExpireTime), this);
                    }
                } catch (Exception e) {
                    log.error(String.format(
                            "renew lock occured an exception, key = %s, token = %s, currentThread = %s",
                            realKey, token, Thread.currentThread().getName()), e);
                    // 失败则直接退出线程
                    break;
                }
            }
        }

        @Override
        public String toString() {
            return "RenewThread{" +
                    "realKey='" + realKey + '\'' +
                    ", token='" + token + '\'' +
                    ", expireTime=" + expireTime +
                    ", timeUnit=" + timeUnit +
                    ", mainThread=" + mainThread +
                    ", renewing=" + renewing +
                    '}';
        }
    }

}
